/*
    Copyright © 2010  Ahti Legonkov
    
    This file is part of j+-.
    
    j+- is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.
    
    j+- is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.
    
    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

package cx.ath.skyflyer.javapm.visitor;

import cx.ath.skyflyer.javapm.util.MessageCollection;
import japa.parser.ASTHelper;
import japa.parser.ast.Node;
import japa.parser.ast.TypeParameter;
import japa.parser.ast.body.BodyDeclaration;
import japa.parser.ast.body.ClassOrInterfaceDeclaration;
import japa.parser.ast.body.DestructorDeclaration;
import japa.parser.ast.body.FieldDeclaration;
import japa.parser.ast.body.MethodDeclaration;
import japa.parser.ast.body.ModifierSet;
import japa.parser.ast.body.Parameter;
import japa.parser.ast.body.VariableDeclarator;
import japa.parser.ast.expr.AnnotationExpr;
import japa.parser.ast.expr.FieldAccessExpr;
import japa.parser.ast.expr.MethodCallExpr;
import japa.parser.ast.expr.NameExpr;
import japa.parser.ast.expr.Expression;
import japa.parser.ast.expr.ThisExpr;
import japa.parser.ast.expr.VariableDeclarationExpr;
import japa.parser.ast.stmt.BlockStmt;
import japa.parser.ast.stmt.ExpressionStmt;
import japa.parser.ast.stmt.Statement;
import japa.parser.ast.stmt.TryStmt;
import japa.parser.ast.type.ClassOrInterfaceType;
import japa.parser.ast.type.PrimitiveType;
import japa.parser.ast.type.Type;
import java.util.ArrayList;
import java.util.List;

/**
 * Each visitor returns a transformed node.
 * @author Ahti Legonkov
 */
public class DestructorTransformVisitor extends IdentityTransformVisitor
{
    private final MessageCollection messages = new MessageCollection();
    
    private MethodDeclaration createVirtualDestructor(ClassOrInterfaceDeclaration n)
    {
        //
        //        public void __destructor__() {
        //            C.__destructor__(this);
        //        }
        //

        // create a method
        MethodDeclaration method = new MethodDeclaration(
                ModifierSet.PUBLIC,
                ASTHelper.VOID_TYPE,
                "__destructor__");

        // add a body to the method
        BlockStmt block = new BlockStmt();
        method.setBody(block);

        // add a statement do the method body
        NameExpr clazz = new NameExpr(n.getName());
        MethodCallExpr call = new MethodCallExpr(clazz, "__destructor__");
        ASTHelper.addArgument(call, new ThisExpr());
        ASTHelper.addStmt(block, call);

        return method;
    }

    private MethodDeclaration createStaticDestructor(ClassOrInterfaceDeclaration n
            , boolean callCustom
            , boolean callMembers
            )
    {
        //
        //      protected static void __destructor__(C o) {
        //          o.__destructor__custom_code__C();
        //          __destructor__members(o);
        //          __destructor__superclass(o);
        //      }
        //

        // create a method
        MethodDeclaration method = new MethodDeclaration(
                ModifierSet.PROTECTED | ModifierSet.STATIC,
                ASTHelper.VOID_TYPE,
                "__destructor__");

        // add a body to the method
        BlockStmt block = new BlockStmt();
        method.setBody(block);

        // add a parameter to the method
        Parameter param = ASTHelper.createParameter(ASTHelper.createReferenceType(n.getName(), 0), "o");
        param.setVarArgs(false);
        ASTHelper.addParameter(method, param);

        if (callCustom) {
            // add a statement to the method body
            NameExpr clazz = new NameExpr("o");
            MethodCallExpr call = new MethodCallExpr(clazz, "__destructor__custom_code__"+n.getName());
            ASTHelper.addStmt(block, call);
        }

        if (callMembers) {
            // add a statement to the method body
            NameExpr clazz = new NameExpr(n.getName());
            MethodCallExpr call = new MethodCallExpr(clazz, "__destructor__members");
            ASTHelper.addArgument(call, new NameExpr("o"));
            ASTHelper.addStmt(block, call);
        }

        // add a statement to the method body
        NameExpr clazz = new NameExpr(n.getName());
        MethodCallExpr call = new MethodCallExpr(clazz, "__destructor__superclass");
        ASTHelper.addArgument(call, new NameExpr("o"));
        ASTHelper.addStmt(block, call);

        return method;
    }

    private MethodDeclaration createDestructorCustomCode(ClassOrInterfaceDeclaration n)
    {
        //    private static void __destructor__custom_code__C(C o) {
        //        the original block from ~ClassName
        //    }

        MethodDeclaration method = new MethodDeclaration(
                ModifierSet.PRIVATE,
                ASTHelper.VOID_TYPE,
                "__destructor__custom_code__"+n.getName());

        // find destructor body
        FindDestructorVisitor destructorFinder = new FindDestructorVisitor();
        destructorFinder.visit(n, null);
        if (destructorFinder.getDestructorDeclaration() == null)
            return null;

        DestructorDeclaration dd = destructorFinder.getDestructorDeclaration();
        BlockStmt block = dd.getBlock();
        method.setBody(block);

        return method;
    }

    private MethodDeclaration createMembersDestructor(ClassOrInterfaceDeclaration n)
    {
        //        private static void __destructor__members(C o) {
        //            assert(o.x != null);
        //            jpm.Destructor.invoke(o.x);
        //        }

        MethodDeclaration method = new MethodDeclaration(
                ModifierSet.PRIVATE | ModifierSet.STATIC,
                ASTHelper.VOID_TYPE,
                "__destructor__members");

        // add a parameter to the method
        Parameter param = ASTHelper.createParameter(ASTHelper.createReferenceType(n.getName(), 0), "o");
        param.setVarArgs(false);
        ASTHelper.addParameter(method, param);

        // find auto-members
        AutoFieldVisitor afv = new AutoFieldVisitor();
        afv.visit(n, afv);

        // add destructors in reverse order.
        BlockStmt block = new BlockStmt();
        method.setBody(block);
        FieldDeclaration autoFields = afv.getAutoFields();
        if (autoFields == null)
            return null;

        final Type type = autoFields.getType();

        if (type instanceof PrimitiveType) {
            messages.addError(
                    "Can't use 'auto' modifier on '"+type+"' type.", "",
                    type.getBeginLine(), type.getBeginColumn());
        }

        for (VariableDeclarator var : autoFields.getVariables())
        {
            // add 'assert (o != null);' to the method body
            MethodCallExpr call = new MethodCallExpr(null, "assert");
            ASTHelper.addArgument(call, new NameExpr("o."+var.getId().getName()+" != null"));
            ASTHelper.addStmt(block, call);

            // add 'jpm.Destructor.invoke(o.x);' to the method body
            NameExpr clazz = new NameExpr("jpm.Destructor");
            call = new MethodCallExpr(clazz, "invoke");
            FieldAccessExpr fieldAccess = new FieldAccessExpr();
            fieldAccess.setScope(new NameExpr("o"));
            fieldAccess.setField(var.getId().getName());
            ASTHelper.addArgument(call, fieldAccess);
            ASTHelper.addStmt(block, call);
        }

        return method;
    }

    private MethodDeclaration createSuperDestructor(ClassOrInterfaceDeclaration n)
    {
        //        private static void __destructor__superclass(C o) {
        //            Destructor.invoke((Base)b, Base.class);
        //        }

        // create a method
        MethodDeclaration method = new MethodDeclaration(
                ModifierSet.PRIVATE | ModifierSet.STATIC,
                ASTHelper.VOID_TYPE,
                "__destructor__superclass");

        // add a parameter to the method
        Parameter param = ASTHelper.createParameter(ASTHelper.createReferenceType(n.getName(), 0), "o");
        param.setVarArgs(false);
        ASTHelper.addParameter(method, param);

        // add a body to the method
        BlockStmt block = new BlockStmt();
        method.setBody(block);

        ExtendsDestructibleVisitor edv = new ExtendsDestructibleVisitor();
        edv.visit(n, null);

        if (edv.getSuperClass() != null)
        {
            final String baseName = edv.getSuperClass().getName();

            // add 'jpm.Destructor.invoke((Base)o, Base.class);' to the method body
            NameExpr clazz = new NameExpr("jpm.Destructor");
            MethodCallExpr call = new MethodCallExpr(clazz, "invoke");
            ASTHelper.addArgument(call, new NameExpr("("+baseName+")o"));
            ASTHelper.addArgument(call, new NameExpr(baseName+".class"));
            ASTHelper.addStmt(block, call);
        }

        return method;
    }

    public MessageCollection getMessages() {
        return messages;
    }

    @Override
    public Node visit(ClassOrInterfaceDeclaration n, Object arg) {
        // add destructor functions to AST if either:
        // - ~Something form of destructor is declared;
        // - an auto field is declared.

        if (n.getMembers() != null) {
            List<BodyDeclaration> members = new ArrayList<BodyDeclaration>();
            for (BodyDeclaration member : n.getMembers()) {
                members.add((BodyDeclaration)member.accept(this, n));
            }
            n.setMembers(members);
        }

        // The interface does not need a destructor implementation. In fact
        // it's an error to have methods with bodies in an interface.
        if (!n.isInterface()) {
            boolean callCustom = false;
            boolean callMember = false;

            MethodDeclaration method = createDestructorCustomCode(n);
            if (method != null) {
                ASTHelper.addMember(n, method);
                callCustom = true;
            }

            method = createMembersDestructor(n);
            if (method != null) {
                ASTHelper.addMember(n, method);
                callMember = true;
            }

            method = createSuperDestructor(n);
            ASTHelper.addMember(n, method);

            method = createStaticDestructor(n, callCustom, callMember);
            ASTHelper.addMember(n, method);

            method = createVirtualDestructor(n);
            ASTHelper.addMember(n, method);

            List<ClassOrInterfaceType> implements_ = n.getImplements();
            if (implements_ == null) {
                implements_ = new ArrayList<ClassOrInterfaceType>();
                n.setImplements(implements_);
            }

            ClassOrInterfaceType interface_ = new ClassOrInterfaceType();
            interface_.setName("jpm.IDestructible");
            implements_.add(interface_);
            
            // remove JPM destructor.
            ASTHelper.removeDestructor(n);
        }

        return n;
    }

    /** Returns a transformed block statement */
    @Override
    public Node visit(BlockStmt n, Object arg) {
        if (n.getStmts() == null)
            return n;

        // Transformed block statement.
        BlockStmt res = new BlockStmt();
        List<Statement> stmts = new ArrayList<Statement>();
        res.setStmts(stmts);

        // true if transforming, false, if just copying the statements to the 
        // created tryBlock.
        boolean isTransforming = true;
        TryStmt tryStmt = null;
        BlockStmt tryBlock = null;

        for (Statement s : n.getStmts()) {
            if (isTransforming) {
                stmts.add((Statement)s.accept(this, arg));
            }
            else {
                assert(tryStmt != null);
                assert(tryBlock != null);
                
                //Statement stmt = (Statement)s.accept(this, arg);
                if (tryBlock.getStmts() == null) {
                    tryBlock.setStmts(new ArrayList<Statement>());
                }
                tryBlock.getStmts().add(s.clone());
                continue;
            }

            if (!(s instanceof ExpressionStmt))
                continue;

            ExpressionStmt es = (ExpressionStmt)s;
            Expression ex = es.getExpression();
            if (!(ex instanceof VariableDeclarationExpr))
                continue;

            VariableDeclarationExpr vde = (VariableDeclarationExpr)ex;
            if (!ModifierSet.isAuto(vde.getModifiers()))
                continue;

            tryStmt = new TryStmt();
            stmts.add(tryStmt);
            tryBlock = new BlockStmt();
            BlockStmt finallyBlock = new BlockStmt();
            tryStmt.setFinallyBlock(finallyBlock);

            // XXX: Why is there no reverse iterator?
            java.util.Collections.reverse(vde.getVars());

            // Don't add destructor invocation for primitive types
            if (!(vde.getType() instanceof PrimitiveType)) {
                for (VariableDeclarator var : vde.getVars()) {
                    // add a statement to the method body
                    NameExpr clazz = new NameExpr("jpm.Destructor");
                    MethodCallExpr call = new MethodCallExpr(clazz, "invoke");
                    ASTHelper.addArgument(call, new NameExpr(var.getId().getName()));
                    ASTHelper.addStmt(finallyBlock, call);
                }
            }
            else {
                //System.err.println("Error: can't use auto for primitive types.");
                messages.addWarning2(
                        "Ignoring 'auto' for primitive types.", "",
                        vde.getType().getBeginLine(), vde.getType().getBeginColumn());
            }
            
            // XXX: restore original order because it's not possible to
            // reverse iterate over the container :(
            java.util.Collections.reverse(vde.getVars());

            isTransforming = false;
        }

        if (tryStmt != null) {
            tryStmt.setTryBlock((BlockStmt)tryBlock.accept(this, arg));
        }

        return res;
    }

    @Override
    public Node visit(MethodDeclaration n, Object arg) {
        if (n.getAnnotations() != null) {
            for (AnnotationExpr a : n.getAnnotations()) {
                a.accept(this, arg);
            }
        }
        if (n.getTypeParameters() != null) {
            for (TypeParameter t : n.getTypeParameters()) {
                t.accept(this, arg);
            }
        }
        n.getType().accept(this, arg);
        if (n.getParameters() != null) {
            for (Parameter p : n.getParameters()) {
                p.accept(this, arg);
            }
        }
        if (n.getThrows() != null) {
            for (NameExpr name : n.getThrows()) {
                name.accept(this, arg);
            }
        }
        if (n.getBody() != null) {
            n.setBody((BlockStmt)n.getBody().accept(this, arg));
        }
        return n;
    }
}
